/**
 * 
 */
package jmine.tec.utils.collection.impl;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Special {@link Map} implementation that respects class hierarchy. That is, a key is considered in the map if its superclasses and its
 * interfaces are in the map, in this particular order, so if the {@link Object} class is put, no calls to {@link #get(Object)} will return
 * <code>null</code> anymore.
 * <p>
 * This class also mantains a cache of shortcuts to classes that are found, so lookup to superclasses and interfaces happens only once. This
 * cache is mantained as long as the instance does not change, in witch case the cache is cleared.
 * <p>
 * This implementation is not thread-safe and has to be synchronized from outside.
 * 
 * @author takeshi
 * @version 23/08/2007
 * @param <E> the type of the map
 */
public class ClassHierarchyMap<E> implements Serializable, Map<Class<?>, E> {

    private static final long serialVersionUID = 1358860190013623362L;

    private final Map<Class<?>, E> delegate;

    private final Map<Class<?>, E> shortCuts;

    /**
     * C'tor By default, creates a concurrent map
     */
    public ClassHierarchyMap() {
        this(true);
    }

    /**
     * C'tor com capacidade para construir este mapa de modo que ele possa ser usado por varias threads ao mesmo tempo
     * 
     * @param concurrent boolean se true, vai usar mapas concorrentes
     */
    public ClassHierarchyMap(final boolean concurrent) {
        if (concurrent) {
            this.delegate = new ConcurrentHashMap<Class<?>, E>();
            this.shortCuts = new ConcurrentHashMap<Class<?>, E>();
        } else {
            this.delegate = new HashMap<Class<?>, E>();
            this.shortCuts = new HashMap<Class<?>, E>();
        }
    }

    /**
     * C'tor for subclasses to use diff {@link Map} implementations.
     * 
     * @param delegate the delegate {@link Map}
     * @param shortCuts the shortcut {@link Map}
     */
    public ClassHierarchyMap(final Map<Class<?>, E> delegate,
            final Map<Class<?>, E> shortCuts) {
        super();
        this.delegate = delegate;
        this.shortCuts = shortCuts;
    }

    /**
     * 
     */
    public void clear() {
        this.delegate.clear();
        this.shortCuts.clear();
    }

    /**
     * Gets an E associated with the given key
     * 
     * @param key the key to lookup
     * @return E or <code>null</code> if none found
     * @see java.util.Map#containsKey(java.lang.Object)
     */
    public boolean containsKey(final Object key) {
        return this.get(key) != null;
    }

    /**
     * Checks for presence of the given value.
     * 
     * @param value the value
     * @return boolean
     * @see java.util.Map#containsValue(java.lang.Object)
     */
    public boolean containsValue(final Object value) {
        return this.delegate.containsValue(value);
    }

    /**
     * Gets the entry set
     * 
     * @return the entry set
     * @see java.util.Map#entrySet()
     */
    public Set<Entry<Class<?>, E>> entrySet() {
        return this.delegate.entrySet();
    }

    /**
     * Gets the object associated with the given key. The key must be a {@link Class} instance otherwise a {@link ClassCastException} will
     * be thrown.
     * <p>
     * If the given class if not associated with any object, this implementation will try to lookup superclasses and interfaces in this
     * order. A shortcut will optionally be set so further lookups will be potentially faster.
     * 
     * @param key the key
     * @return E or <code>null</code> if it cannot be found.
     * @see java.util.Map#get(java.lang.Object)
     */
    public E get(final Object key) {
        final E toRet = this.shortCuts.get(key);
        if (toRet != null) {
            return toRet;
        }
        final Class<?> type = (Class<?>) key;
        final E e = this.lookup(type);
        if (e != null) {
            this.shortCuts.put(type, e);
        }
        return e;
    }

    /**
     * Returns wheter or not this {@link Map} is empty.
     * 
     * @return boolean
     * @see java.util.Map#isEmpty()
     */
    public boolean isEmpty() {
        return this.delegate.isEmpty();
    }

    /**
     * Returns the key set.
     * 
     * @return the set with the keys
     * @see java.util.Map#keySet()
     */
    public Set<Class<?>> keySet() {
        return this.delegate.keySet();
    }

    /**
     * Associates the given {@link Class} with a particular value. This method clears the cache.
     * 
     * @param key the {@link Class} to be used as a key
     * @param value the value to associate to.
     * @return E the value previously associated with the same key
     * @see java.util.Map#put(java.lang.Object, java.lang.Object)
     */
    public E put(final Class<?> key, final E value) {
        try {
            return this.delegate.put(key, value);
        } finally {
            this.shortCuts.clear();
        }
    }

    /**
     * Puts all the entries of the given {@link Map} on this {@link Map}. This method forces the cache to be cleared.
     * 
     * @param m the other map
     * @see java.util.Map#putAll(java.util.Map)
     */
    public void putAll(final Map<? extends Class<?>, ? extends E> m) {
        try {
            this.delegate.putAll(m);
        } finally {
            this.shortCuts.clear();
        }
    }

    /**
     * Removes an entry from this map, returning the old value if any. This method forces the cache to be cleared.
     * 
     * @param key the key
     * @return E or <code>null</code> if the key was not associated with any value
     * @see java.util.Map#remove(java.lang.Object)
     */
    public E remove(final Object key) {
        boolean clear = false;
        if (this.delegate.containsKey(key)) {
            clear = true;
        }
        try {
            return this.delegate.remove(key);
        } finally {
            if (clear) {
                this.shortCuts.clear();
            }
        }
    }

    /**
     * Returns the size of this map.
     * 
     * @return int
     * @see java.util.Map#size()
     */
    public int size() {
        return this.delegate.size();
    }

    /**
     * Return a collection with all the values.
     * 
     * @return Collection
     * @see java.util.Map#values()
     */
    public Collection<E> values() {
        return this.delegate.values();
    }

    /**
     * @param type type
     * @return E
     */
    private E lookup(final Class<?> type) {
        if (type == null) {
            return null;
        }
        Class<?> toLookup = type;
        E toReturn = null;
        while (toLookup != null) {
            toReturn = this.delegate.get(toLookup);
            if (toReturn != null) {
                return toReturn;
            }
            final Class<?>[] interfaces = toLookup.getInterfaces();
            for (final Class<?> class1 : interfaces) {
                toReturn = this.delegate.get(class1);
                if (toReturn != null) {
                    return toReturn;
                }
            }
            for (final Class<?> class1 : interfaces) {
                toReturn = this.lookupSuperInterfaces(class1);
                if (toReturn != null) {
                    return toReturn;
                }
            }
            toLookup = toLookup.getSuperclass();
        }
        return null;
    }

    /**
     * Procura superinterfaces da interface passada
     * 
     * @param base a interface base
     * @return E ou <code>null</code> se nenhum encontrado
     */
    private E lookupSuperInterfaces(final Class<?> base) {
        if (base == null) {
            return null;
        }
        Class<?>[] ifs = base.getInterfaces();
        for (Class<?> class1 : ifs) {
            E e = this.delegate.get(class1);
            if (e != null) {
                return e;
            }
            e = this.lookupSuperInterfaces(class1);
            if (e != null) {
                return e;
            }
        }
        return null;
    }

}
